package cn.itcast.hotel.service.impl;

import cn.itcast.hotel.mapper.HotelMapper;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.HotelDoc;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.pojo.RequestParams;
import cn.itcast.hotel.service.IHotelService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.lucene.search.function.FieldValueFactorFunction;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestParam;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class HotelService extends ServiceImpl<HotelMapper, Hotel> implements IHotelService {

    @Autowired
    private RestHighLevelClient client;
    private String HOTEL_STR = "hotel";



    @Override
    public PageResult queryHotels(RequestParams param) {

        try {
            SearchRequest searchRequest = new SearchRequest (HOTEL_STR);
            queryBuild (param,searchRequest);
            //分页
            int page = param.getPage ();
            int size = param.getSize ();
            searchRequest.source ().from ((page - 1) * size).size (size);
            //高亮
            //高亮默认是添加<em></em>标签，需要前端对<em>标签进行处理，如果不想处理也可以使用.preTags ()方法自定义标签
            searchRequest.source ().highlighter (new HighlightBuilder ()
                                                         .field ("name")
                                                         .field ("brand")
                                                         .requireFieldMatch (false));
            //地理位置排序
            if (!StringUtils.isEmpty (param.getLocation ())){
                searchRequest.source ().sort (SortBuilders
                                .geoDistanceSort ("location",new GeoPoint (param.getLocation ()))
                                .order (SortOrder.ASC)
                                .unit (DistanceUnit.KILOMETERS));
            }
            //根据价格排序
            if (!StringUtils.isEmpty (param.getSortBy ()) && "price".equals (param.getSortBy ())){
                searchRequest.source ().sort ("price",SortOrder.ASC);
            }
            //根据评分排序
            if (!StringUtils.isEmpty (param.getSortBy ()) && "score".equals (param.getSortBy ())){
                searchRequest.source ().sort ("score",SortOrder.DESC);
            }
            SearchResponse search = client.search (searchRequest, RequestOptions.DEFAULT);
            return handleRespData (search);
        } catch (IOException e) {
           throw new RuntimeException ("搜索数据失败",e);
        }
    }
    @Override
    public Map<String, List<String>> filters(RequestParams param) {

        try {
            SearchRequest searchRequest = new SearchRequest (HOTEL_STR);
            searchRequest.source ().size ();
            //条件构造
            queryBuild (param, searchRequest);
            //聚合
            buildAggregations (searchRequest);
            SearchResponse search = client.search (searchRequest, RequestOptions.DEFAULT);
            Aggregations aggregations = search.getAggregations ();
            Map<String, List<String>> map = new HashMap<> ();
//            List<String> list = new ArrayList<> ();
            //解析城市
            map.put ("brand",getAggregationByName (aggregations,"brandAgg"));
            //解析品牌
            map.put ("city",getAggregationByName (aggregations,"cityAgg"));
            //解析星级
            map.put ("starName",getAggregationByName (aggregations,"starNameAgg"));
            return map;
        } catch (IOException e) {
            throw new RuntimeException ("聚合搜索发生异常",e);
        }


    }

    @Override
    public List<String> suggestion(String key) {

        try {
            SearchRequest searchRequest = new SearchRequest (HOTEL_STR);
            searchRequest.source ().suggest (new SuggestBuilder ()
                                                     .addSuggestion (
                                                             "suggestion",
                                                             SuggestBuilders.completionSuggestion ("suggestion")
                                                                     .prefix (key)
                                                                     .skipDuplicates (true)
                                                                     .size (10)));
            SearchResponse search = client.search (searchRequest, RequestOptions.DEFAULT);
            Suggest suggest = search.getSuggest ();
            CompletionSuggestion suggestion = suggest.getSuggestion ("suggestion");
            List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions ();
            List<String> list = new ArrayList<> ();
            for (CompletionSuggestion.Entry.Option option : options) {
                list.add (option.getText ().string ());
            }
            return list;
        } catch (IOException e) {
            throw new RuntimeException (e);
        }

    }

    @Override
    public void deleteById(Long id) {
        DeleteRequest deleteRequest = new DeleteRequest (HOTEL_STR,id.toString ());
        try {
            client.delete (deleteRequest,RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException ("删除文档异常："+e);
        }
    }

    @Override
    public void InsertOrUpdateById(Long id) {
        Hotel hotel = this.baseMapper.selectById (id);
        if (hotel == null){
            throw new RuntimeException ("数据对象为空");
        }
        HotelDoc hotelDoc = new HotelDoc (hotel);
        String jsonHotel = JSON.toJSONString (hotelDoc);
        IndexRequest request = new IndexRequest (HOTEL_STR).id (id.toString ());
        request.source (jsonHotel, XContentType.JSON);
        try {
            client.index (request,RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException ("es数据更新异常"+e);
        }
    }

    private List<String> getAggregationByName(Aggregations aggregations, String aggName) {
        Terms terms = aggregations.get (aggName);
        List<String> list = new ArrayList<> ();
        for (Terms.Bucket bucket : terms.getBuckets ()) {
            String keyAsString = bucket.getKeyAsString ();
            list.add (keyAsString);
        }
        return list;
    }

    private void buildAggregations(SearchRequest searchRequest) {
        searchRequest.source ().aggregation (AggregationBuilders.terms ("brandAgg").field ("brand").size (100));
        searchRequest.source ().aggregation (AggregationBuilders.terms ("cityAgg").field ("city").size (100));
        searchRequest.source ().aggregation (AggregationBuilders.terms ("starNameAgg").field ("starName").size (100));
    }

    /**
     * 条件构造
     * @param params
     * @param searchRequest
     */
    public void queryBuild(RequestParams params,SearchRequest searchRequest){

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery ();
        if (StringUtils.isEmpty (params.getKey ())){
            //无搜索条件
            boolQueryBuilder.must (QueryBuilders.matchAllQuery ());

        }else {
            //有搜索条件
            boolQueryBuilder.must (QueryBuilders.matchQuery ("all",params.getKey ()));
        }
        //城市
        if(!StringUtils.isEmpty (params.getCity ())){
            boolQueryBuilder.filter (QueryBuilders.termQuery ("city",params.getCity ()));
        }
        //品牌
        if (!StringUtils.isEmpty (params.getBrand ())){
            boolQueryBuilder.filter (QueryBuilders.termQuery ("brand",params.getBrand ()));
        }
        //星级
        if (!StringUtils.isEmpty (params.getStarName ())){
            boolQueryBuilder.filter (QueryBuilders.termQuery ("starName",params.getStarName ()));
        }
        //价格范围
        Integer maxPrice = params.getMaxPrice ();
        Integer minPrice = params.getMinPrice ();
        if (maxPrice != null && minPrice != null){
            maxPrice = maxPrice == 0 ? Integer.MAX_VALUE : maxPrice;
            boolQueryBuilder.filter (QueryBuilders.rangeQuery ("price").gte (minPrice).lte (maxPrice));
        }

        //算分函数
        FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery (
                boolQueryBuilder,
                new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder (
                                //isAD字段为true的文档参与算分
                                QueryBuilders.termQuery ("isAD",true),
                                //score字段的值参与算分
                                ScoreFunctionBuilders.fieldValueFactorFunction ("score")
                                        //结果处理方式
                                        .modifier (FieldValueFactorFunction.Modifier.NONE)
                        )
                }
                //计算方式为sum
        ).scoreMode (FunctionScoreQuery.ScoreMode.SUM);
        searchRequest.source ().query (functionScoreQueryBuilder);
    }

    /**
     * 对查询结果进行处理
     * @param search
     * @return
     */
    public PageResult handleRespData(SearchResponse search){
        //总条数
        long total = search.getHits ().getTotalHits ().value;
        //总文档数组
        SearchHit[] hits = search.getHits ().getHits ();
        List<HotelDoc> hotelDocs = new ArrayList<> (hits.length);
        for (SearchHit hit : hits) {
            HotelDoc hotelDoc = JSON.parseObject (hit.getSourceAsString (), HotelDoc.class);

            //高亮信息
            Map<String, HighlightField> highlightFields = hit.getHighlightFields ();
            if (!CollectionUtils.isEmpty (highlightFields)){
                HighlightField highlightField = highlightFields.get ("name");
                if (highlightField != null){
                    String name = highlightField.getFragments ()[0].string ();
                    hotelDoc.setName (name);
                }
                HighlightField highlightFieldBrand = highlightFields.get ("brand");
                if (highlightFieldBrand != null){
                    String brand = highlightFieldBrand.getFragments ()[0].string ();
                    hotelDoc.setBrand (brand);
                }
            }
            //排序信息
            Object[] sortValues = hit.getSortValues ();
            if (sortValues.length > 0){
                hotelDoc.setDistance (sortValues[0]);
            }
            hotelDocs.add (hotelDoc);
        }
        return new PageResult (total,hotelDocs);
    }
}
